// Attribution:  Some of the content view logic was copied or inspired from the custom window article on Matt Gallagher's excellent Cocoa wih Love blog (http://cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html)


#import "SMRPopoverWindow.h"
#import "SMRPopoverFrameView.h"
#import "NSWindow+SMRAdditions.h"


#pragma mark Constants
const CGFloat SMRPopoverWindowScreenPadding = 10.0f;
static NSString* const SMRPopoverWindowPreferredContentSizeKey = @"SMRPopoverWindowPreferredContentSizeKey";
static NSString* const SMRPopoverWindowPreferredArrowDirectionKey = @"SMRPopoverWindowPreferredArrowDirectionKey";


#pragma mark -
@interface SMRPopoverWindow ()
@property (nonatomic, assign) NSRect targetScreenRect;
@property (nonatomic, assign) BOOL presentationOrientationDetermined;
@end


#pragma mark -
@interface SMRPopoverWindow (SMRPopoverWindowPrivateMethods)
-(void)commonInit_;
-(NSSize)preferredFrameSizeWithoutArrow_;
-(NSSize)preferredFrameSize_;
-(void)establishFrameWithPermittedDirections_:(SMRPopoverArrowDirection)permittedArrowDirections;
-(void)updateFrame_;
-(SMRPopoverArrowDirection)bestArrowDirectionWithPermittedArrowDirections_:(SMRPopoverArrowDirection)permittedArrowDirections;
-(SMRPopoverArrowDirection)nextArrowDirectionFromCompositeDirection_:(SMRPopoverArrowDirection)compositeDirection;
-(CGFloat)bestArrowPositionWithArrowDirection_:(SMRPopoverArrowDirection)bestArrowDirection;
-(NSRect)popoverFrameWithArrowDirection_:(SMRPopoverArrowDirection)arrowDirection withArrowPosition:(CGFloat)bestArrowPosition;
@end


#pragma mark -
@implementation SMRPopoverWindow

@synthesize preferredContentSize;
@synthesize preferredArrowDirection;
@synthesize shouldBecomeKey;
@synthesize targetScreenRect;
@synthesize presentationOrientationDetermined;


#pragma mark -
#pragma mark Class methods

+(Class)PopoverFrameViewClass {
	return [SMRPopoverFrameView class];
}


#pragma mark -
#pragma mark Initialization

-(id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation {
	if (!(self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:deferCreation]))
		return nil;
	[self setHasShadow:YES];
	[self setBackgroundColor:[NSColor clearColor]];
	[self setLevel:NSFloatingWindowLevel];
	[self setOpaque:NO];
	[self setShowsResizeIndicator:NO];
	[self setMovable:NO];
	[self setExcludedFromWindowsMenu:YES];
	[self setShouldBecomeKey:YES];
	
	return self;
}


#pragma mark -
#pragma mark Accessors

-(CGFloat)borderWidth {
	return [(SMRPopoverFrameView *)[self contentView] borderWidth];
}

-(NSSize)arrowSize {
	return [(SMRPopoverFrameView *)[self contentView] arrowSize];
}

-(CGFloat)arrowPosition {
	return [(SMRPopoverFrameView *)[self contentView] arrowPosition];
}

-(void)setArrowPosition:(CGFloat)newPosition {
	[(SMRPopoverFrameView *)[self contentView] setArrowPosition:newPosition];
}

-(SMRPopoverArrowDirection)arrowDirection {
	return [(SMRPopoverFrameView *)[self contentView] arrowDirection];
}

-(void)setArrowDirection:(SMRPopoverArrowDirection)newDirection {
	[(SMRPopoverFrameView *)[self contentView] setArrowDirection:newDirection];
}

-(void)setPreferredContentSize:(NSSize)contentSize {
	preferredContentSize = contentSize;
	if ([self presentationOrientationDetermined])
		[self updateFrame_];
}


#pragma mark -
#pragma mark Display 

-(void)presentPopoverFromRect:(NSRect)rect inView:(NSView *)view permittedArrowDirections:(SMRPopoverArrowDirection)arrowDirections animated:(BOOL)animated {
	NSRect viewRectBaseCoordinates = [view convertRectToBase:rect];
	NSRect viewRectScreenCoordinates = [[view window] smrConvertRectBaseToScreen:viewRectBaseCoordinates];
	[self setTargetScreenRect:viewRectScreenCoordinates];
	[self establishFrameWithPermittedDirections_:arrowDirections];
	[[view window] addChildWindow:self ordered:NSWindowAbove];
	if ([self shouldBecomeKey])
		[self makeKeyAndOrderFront:nil];
	else 
		[self orderFront:nil];
}


#pragma mark -
#pragma mark NSWindow overrides

-(void)setContentView:(NSView *)aView  { 
	if (NSEqualSizes(NSZeroSize, [self preferredContentSize]))
		[self setPreferredContentSize:[aView frame].size];
	SMRPopoverFrameView *frameView = [[[[self class] PopoverFrameViewClass] alloc] initWithFrame:NSZeroRect];
	[super setContentView:[frameView autorelease]];
	NSRect windowFrame = [self frame];
	windowFrame.origin = NSZeroPoint;
	[aView setFrame:windowFrame];
	[aView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
	[frameView addSubview:aView];
}

-(BOOL)canBecomeKeyWindow {
	return [self shouldBecomeKey];
}

-(void)resignKeyWindow {
	[super resignKeyWindow];
	[self dismissPopoverAnimated:YES];
}

-(void)dismissPopoverAnimated:(BOOL)shouldAnimate {
	[self setPresentationOrientationDetermined:NO];
	[[self parentWindow] removeChildWindow:self];
	[self orderOut:nil];
}


#pragma mark -
#pragma mark Event handling

-(void)cancelOperation:(id)sender {
	[[self parentWindow] makeKeyWindow];
}


#pragma mark -
#pragma mark Private methods

-(NSSize)preferredFrameSizeWithoutArrow_ {
	NSSize preferredFrameSize = [self preferredContentSize];
	CGFloat borderWidth = [self borderWidth];
	preferredFrameSize.height += 2 * borderWidth;
	preferredFrameSize.width += 2 * borderWidth;
	return preferredFrameSize;
}

-(NSSize)preferredFrameSize_ {
	NSSize preferredFrameSize = [self preferredFrameSizeWithoutArrow_];
	SMRPopoverArrowDirection currentArrowDirection = [self arrowDirection];
	if (currentArrowDirection & SMRPopoverArrowDirectionUnknown)
		return preferredFrameSize;
	BOOL arrowIsVertical = currentArrowDirection & SMRPopoverArrowDirectionUp || currentArrowDirection & SMRPopoverArrowDirectionDown;
	if (arrowIsVertical)
		preferredFrameSize.height += [self arrowSize].height;
	else 
		preferredFrameSize.width += [self arrowSize].height;
	return preferredFrameSize;
}

-(void)establishFrameWithPermittedDirections_:(SMRPopoverArrowDirection)permittedArrowDirections {
	SMRPopoverArrowDirection arrowDirection = [self bestArrowDirectionWithPermittedArrowDirections_:permittedArrowDirections];
	[self setArrowDirection:arrowDirection];
	CGFloat arrowPosition = [self bestArrowPositionWithArrowDirection_:arrowDirection];
	[self setArrowPosition:arrowPosition];
	[self setPresentationOrientationDetermined:YES];
	NSRect popoverFrame = [self popoverFrameWithArrowDirection_:arrowDirection withArrowPosition:arrowPosition];	
	[[self contentView] setNeedsDisplay:YES];
	[self setFrame:popoverFrame display:YES animate:NO];
}

-(void)updateFrame_ {
	NSRect popoverFrame = [self popoverFrameWithArrowDirection_:[self arrowDirection] withArrowPosition:[self arrowPosition]];	
	[[self contentView] setNeedsDisplay:YES];
	[self setFrame:popoverFrame display:YES animate:YES];
}

-(SMRPopoverArrowDirection)bestArrowDirectionWithPermittedArrowDirections_:(SMRPopoverArrowDirection)permittedArrowDirections {
	NSRect fullScreenRect = [[NSScreen mainScreen] visibleFrame];
	SMRPopoverArrowDirection allDirections = permittedArrowDirections;
	SMRPopoverArrowDirection currentArrowDirection;
	SMRPopoverArrowDirection bestArrowDirection = SMRPopoverArrowDirectionUnknown;
	CGFloat currentAvailableLength = 0;
	CGFloat maxAvailableLength = 0;
	NSSize preferredFrameSize = [self preferredFrameSizeWithoutArrow_];
	CGFloat arrowHeight = [self arrowSize].height;
	do {
		currentArrowDirection = [self nextArrowDirectionFromCompositeDirection_:allDirections];
		allDirections ^= currentArrowDirection;
		
		switch (currentArrowDirection) {
			case SMRPopoverArrowDirectionUp:
				currentAvailableLength = NSMinY([self targetScreenRect]) - SMRPopoverWindowScreenPadding;
				break;
			case SMRPopoverArrowDirectionDown:
				currentAvailableLength = NSMaxY(fullScreenRect) - NSMaxY([self targetScreenRect]) - SMRPopoverWindowScreenPadding;
				break;
			case SMRPopoverArrowDirectionLeft:
				currentAvailableLength = NSMaxX(fullScreenRect) - NSMaxX([self targetScreenRect]) - SMRPopoverWindowScreenPadding;
				break;
			case SMRPopoverArrowDirectionRight:
				currentAvailableLength = NSMinX([self targetScreenRect]) - SMRPopoverWindowScreenPadding;
				break;
			default:
				currentAvailableLength = 0;
		}
		BOOL arrowIsVertical = currentArrowDirection & SMRPopoverArrowDirectionUp || currentArrowDirection & SMRPopoverArrowDirectionDown;
		CGFloat minimumApproachDistance = SMRPopoverWindowScreenPadding + [(SMRPopoverFrameView *)[self contentView] minimumArrowPositionFromEdge];
		if (arrowIsVertical) {
			BOOL tooCloseToLeftEdgeOfScreen = NSMidX([self targetScreenRect]) < (NSMinX(fullScreenRect) + minimumApproachDistance);
			BOOL tooCloseToRightEdgeOfScreen = NSMidX([self targetScreenRect]) > (NSMaxX(fullScreenRect) - minimumApproachDistance);
			if (tooCloseToLeftEdgeOfScreen || tooCloseToRightEdgeOfScreen)
				currentAvailableLength = 0;
		}
		if (!arrowIsVertical) {
			BOOL tooCloseToTopEdgeOfScreen = NSMidY([self targetScreenRect]) > (NSMaxY(fullScreenRect) - minimumApproachDistance);
			BOOL tooCloseToBottomEdgeOfScreen = NSMidY([self targetScreenRect]) < (NSMinX(fullScreenRect) + minimumApproachDistance);
			if (tooCloseToTopEdgeOfScreen || tooCloseToBottomEdgeOfScreen)
				currentAvailableLength = 0;
		}
		CGFloat preferredLength = (arrowIsVertical ? preferredFrameSize.height : preferredFrameSize.width) + arrowHeight;
		if (currentAvailableLength >= preferredLength) 
			return currentArrowDirection;
		if (currentAvailableLength > maxAvailableLength) {
			maxAvailableLength = currentAvailableLength;
			bestArrowDirection = currentArrowDirection;
		}
	} while (currentArrowDirection != SMRPopoverArrowDirectionUnknown);
	return bestArrowDirection;
}

-(CGFloat)bestArrowPositionWithArrowDirection_:(SMRPopoverArrowDirection)bestArrowDirection {
	if (bestArrowDirection & SMRPopoverArrowDirectionUnknown)
		return 0.0f;

	NSRect fullScreenRect = [[NSScreen mainScreen] visibleFrame];
	CGFloat (*maxPositionFunction)(NSRect);
	CGFloat (*minPositionFunction)(NSRect);
	CGFloat (*sizeFunction)(NSRect);
	BOOL arrowIsVertical = bestArrowDirection & SMRPopoverArrowDirectionUp || bestArrowDirection & SMRPopoverArrowDirectionDown;
	maxPositionFunction = arrowIsVertical ? &NSMaxX : &NSMaxY;
	minPositionFunction = arrowIsVertical ? &NSMinX : &NSMinY;
	sizeFunction = arrowIsVertical ? &NSWidth : &NSHeight;
	
	CGFloat maxScreen = maxPositionFunction(fullScreenRect) - SMRPopoverWindowScreenPadding;
	CGFloat minScreen = minPositionFunction(fullScreenRect) + SMRPopoverWindowScreenPadding;
	
	NSSize preferredFrameSize = [self preferredFrameSize_];
	NSRect preferredFrameRect = NSZeroRect;
	preferredFrameRect.size = preferredFrameSize;
	CGFloat popoverLength = sizeFunction(preferredFrameRect);
	CGFloat popoverHalfLength = popoverLength / 2;
	
	CGFloat minTarget = minPositionFunction([self targetScreenRect]);
	CGFloat targetLength = sizeFunction([self targetScreenRect]);
	CGFloat midTarget = minTarget + (targetLength / 2);
	
	CGFloat arrowPositionDelta = 0.0f;
	if ((midTarget + popoverHalfLength) > maxScreen) 
		arrowPositionDelta = midTarget + popoverHalfLength - maxScreen;
	else if ((midTarget - popoverHalfLength) < minScreen)
		arrowPositionDelta = midTarget - popoverHalfLength - minScreen;
	CGFloat bestArrowPosition = floor(popoverHalfLength + arrowPositionDelta) + 0.5f;
	return bestArrowPosition;
}

-(NSRect)popoverFrameWithArrowDirection_:(SMRPopoverArrowDirection)bestArrowDirection withArrowPosition:(CGFloat)bestArrowPosition {
	
	NSRect popoverFrame = NSZeroRect;
	NSRect fullScreenRect = NSInsetRect([[NSScreen mainScreen] visibleFrame], SMRPopoverWindowScreenPadding, SMRPopoverWindowScreenPadding);
	NSSize screenSize = fullScreenRect.size;
	NSSize preferredFrameSize = [self preferredFrameSize_];
	CGFloat primaryLength;
	NSPoint targetMidPoint = NSMakePoint(NSMidX([self targetScreenRect]), NSMidY([self targetScreenRect]));
	switch (bestArrowDirection) {
		case SMRPopoverArrowDirectionUp:
			primaryLength = [self targetScreenRect].origin.y - fullScreenRect.origin.y;
			popoverFrame.size.height = fmin(primaryLength, preferredFrameSize.height);
			popoverFrame.size.width = fmin(screenSize.width, preferredFrameSize.width);
			popoverFrame.origin.x = targetMidPoint.x - bestArrowPosition;
			popoverFrame.origin.y = NSMinY([self targetScreenRect]) - popoverFrame.size.height;
			break;
		case SMRPopoverArrowDirectionDown:
			primaryLength = NSMaxY(fullScreenRect) - NSMaxY([self targetScreenRect]);
			popoverFrame.size.height = fmin(primaryLength, preferredFrameSize.height);
			popoverFrame.size.width = fmin(screenSize.width, preferredFrameSize.width);
			popoverFrame.origin.x = targetMidPoint.x - bestArrowPosition;
			popoverFrame.origin.y = NSMaxY([self targetScreenRect]);
			break;
		case SMRPopoverArrowDirectionLeft:
			primaryLength = NSMaxX(fullScreenRect) - NSMaxX([self targetScreenRect]);
			popoverFrame.size.width = fmin(primaryLength, preferredFrameSize.width);
			popoverFrame.size.height = fmin(screenSize.height, preferredFrameSize.height);
			popoverFrame.origin.x = NSMaxX([self targetScreenRect]);
			popoverFrame.origin.y = targetMidPoint.y - bestArrowPosition;
			break;
		case SMRPopoverArrowDirectionRight:
			primaryLength = NSMinX([self targetScreenRect]) - NSMinX(fullScreenRect);
			popoverFrame.size.width = fmin(primaryLength, preferredFrameSize.width);
			popoverFrame.size.height = fmin(screenSize.height, preferredFrameSize.height);
			popoverFrame.origin.x = NSMinX([self targetScreenRect]) - popoverFrame.size.width;
			popoverFrame.origin.y = targetMidPoint.y - bestArrowPosition;
			break;
		default:
			popoverFrame.size.width = fmin(screenSize.width, preferredFrameSize.width);
			popoverFrame.size.height = fmin(screenSize.height, preferredFrameSize.height);
			popoverFrame.origin.x = targetMidPoint.x - (popoverFrame.size.width / 2);
			popoverFrame.origin.y = targetMidPoint.y - (popoverFrame.size.height / 2);
	}
	popoverFrame.origin.x = floor(popoverFrame.origin.x);
	popoverFrame.origin.y = floor(popoverFrame.origin.y);
	return popoverFrame;
}

-(SMRPopoverArrowDirection)nextArrowDirectionFromCompositeDirection_:(SMRPopoverArrowDirection)compositeDirection {
	SMRPopoverArrowDirection currentDirection = [self preferredArrowDirection];
	if (currentDirection == SMRPopoverArrowDirectionUnknown || !(currentDirection & compositeDirection))
		currentDirection = SMRPopoverArrowDirectionUp;
	while (currentDirection) {
		if (currentDirection & compositeDirection)
			return currentDirection;
		currentDirection >>= 1;
	}
	return SMRPopoverArrowDirectionUnknown;
}

@end
